package lacus.wd_cinema.service;

import lacus.wd_cinema.dto.screenings.ScreeningAddRequest;
import lacus.wd_cinema.dto.screenings.ScreeningAddResponse;
import lacus.wd_cinema.entity.Screening;
import lacus.wd_cinema.entity.Ticket;
import lacus.wd_cinema.entity.User;
import lacus.wd_cinema.exception.EntityNotFoundException;
import lacus.wd_cinema.exception.OperationNotAllowedException;
import lacus.wd_cinema.repository.ScreeningRepository;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

/**
 * 与放映相关的业务处理类
 * @author lacus
 * @version 0.6
 * @since 0.3
 */
@Service
@Transactional
public class ScreeningService {
    private final ScreeningRepository screeningRepository;
    private final HallService hallService;
    private final MovieService movieService;
    private final LogService logService;

    public ScreeningService(
            ScreeningRepository screeningRepository,
            HallService hallService,
            MovieService movieService, LogService logService) {
        this.screeningRepository = screeningRepository;
        this.hallService = hallService;
        this.movieService = movieService;
        this.logService = logService;
    }

    /**
     * 生成放映的ID
     * @return 生成的ID
     */
    public String generateScreeningId(String hallId, LocalDateTime startTime) {
        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyyMMdd-HHmm");
        return "SC-" + formatter.format(startTime) + "-" + hallId;
    }

    /** 根据ID获取放映 */
    public Screening getScreeningById(String id) {
        return screeningRepository.findById(id).orElseThrow(
                () -> new EntityNotFoundException("不存在ID为" + id + "的电影")
        );
    }

    /**
     * 搜索一段时间范围内的与电影关联的放映
     * @param movieId 电影ID
     * @param page 页码
     * @param pageSize 页的大小
     * @param startDate 范围起始日期
     * @param endDate 范围结束日期
     * @return 页面
     */
    public Page<Screening> searchScreeningsByMovie(
            long movieId,
            int page, int pageSize,
            String startDate, String endDate) {
        Pageable pageable = PageRequest.of(page - 1, pageSize,
                Sort.by("startTime").ascending());
        if (startDate.isBlank() || endDate.isBlank()) {
            return screeningRepository.findScreeningsByMovieId(movieId, pageable);
        }
        LocalDateTime startTime = LocalDate.parse(startDate).atStartOfDay();
        LocalDateTime endTime = LocalDate.parse(endDate).atTime(23, 59, 59);
        return screeningRepository
                .findScreeningsByMovieIdAndStartTimeBetween(
                        movieId, startTime, endTime, pageable);
    }

    /**
     * 搜索近一周内将进行的放映，
     * 并按照放映的日期进行分组，同时按时间顺序排序
     */
    public List<Map.Entry<LocalDate, List<Screening>>> searchUpcomingScreenings() {
        LocalDateTime startTime = LocalDateTime.now();
        LocalDateTime endTime = LocalDate.now()
                .plusDays(7).atTime(23, 59, 59);
        // 查找所有的近一周内将进行的放映
        List<Screening> allScreenings = screeningRepository
                .findAllByStartTimeBetweenAndStatus(startTime, endTime, Screening.Status.ON_SALE);
        // 按日期进行分组
        Map<LocalDate, List<Screening>> groups = allScreenings.stream()
                .collect(Collectors.groupingBy(
                        (screening) -> screening.getStartTime().toLocalDate()));
        // 将每个组的成员之间按照放映开始时间的顺序排序
        groups.forEach((_, screeningList) ->
                screeningList.sort(Comparator.comparing(Screening::getStartTime)));
        // 最后对组与组之间按日期进行排序
        return groups.entrySet().stream().sorted(Map.Entry.comparingByKey()).toList();
    }

    /** 添加放映，并检查放映是否会有时间冲突 */
    public ScreeningAddResponse addScreening(
            ScreeningAddRequest request, User operator) {
        Screening screening = new Screening();
        screening.setHall(hallService.getHallById(request.getHallId()));
        screening.setMovie(movieService.getMovieById(request.getMovieId()));
        screening.setStartTime(LocalDateTime.of(request.getDate(), request.getStartTime()));
        screening.setEndTime();

        // 检查放映时间是否冲突
        List<Screening> conflicted = screeningRepository.conflictedScreenings(
                request.getHallId(), screening.getStartTime(), screening.getEndTime()
        );
        if (!conflicted.isEmpty()) {
            String message = "放映存在时间冲突: (编号：%s, 放映时间：%s %s - %s)"
                    .formatted(screening.getId(), screening.getStartTime().toLocalDate(),
                            screening.getStartTime().toLocalTime(), screening.getEndTime().toLocalTime());
            throw new OperationNotAllowedException(message);
        }
        screening.parsePrice(request.getTicketPrice());
        screening.setId(generateScreeningId(request.getHallId(), screening.getStartTime()));

        Screening saved = screeningRepository.save(screening);
        logService.log(operator, "添加放映：" + saved.getId()); // 记录业务日志
        return ScreeningAddResponse.from(saved);
    }

    /** 根据ID删除放映 */
    public void deleteScreening(
            String id, User operator) {
        // 检查是否已有票售出，若有则不允许删除
        if (getScreeningById(id).countOfSoldTickets() == 0) {
            screeningRepository.deleteById(id);
            logService.log(operator,  "删除了放映：" + id); // 记录业务日志
        }
        throw new OperationNotAllowedException("不允许删除已有票售出的放映");
    }

    /** 开放售票 */
    public void openForSale(String screeningId, User operator) {
        Screening screening = getScreeningById(screeningId);
        if (screening.isOnSchedule()) {
            screening.setStatus(Screening.Status.ON_SALE);
            logService.log(operator, "对外开放了放映："+ screeningId); // 记录业务日志
        }
    }

    /** 查询放映中的所有座位是否已被预订 */
    public boolean[][] bookedSeats(String screeningId) {
        Screening screening = getScreeningById(screeningId);
        int rows = screening.getHall().getRows();
        int cols = screening.getHall().getCols();
        boolean[][] seats = new boolean[rows][cols];
        for (Ticket ticket : screening.getTickets()) {
            if (ticket.isNotRefunded())
                seats[ticket.getSeatRow()][ticket.getSeatCol()] = true;
        }
        return seats;
    }

    /**
     * 定时更新放映的状态，
     * 设定每5分钟更新一次
     */
    @Scheduled(fixedRate = 5 * 60 * 1000)
    public void updateScreeningStatus() {
        LocalDateTime now = LocalDateTime.now();

        // 更新"停止售票"状态
        LocalDateTime fiveMinutesBefore = now.plusMinutes(10);
        screeningRepository.findAllByStartTimeBetween(now, fiveMinutesBefore)
                .stream()
                .filter(s -> s.getStatus() != Screening.Status.SALE_STOPPED)
                .forEach(screening -> {
                    screening.setStatus(Screening.Status.SALE_STOPPED);
                    screeningRepository.save(screening);
                });

        // 更新"放映中"状态
        screeningRepository.findAllByStartTimeBeforeAndEndTimeAfter(now, now)
                .stream()
                .filter(s -> s.getStatus() != Screening.Status.IN_PROGRESS)
                .forEach(screening -> {
                    screening.setStatus(Screening.Status.IN_PROGRESS);
                    screeningRepository.save(screening);
                });

        // 更新"已结束"状态，并将与放映管理的有效票标记为已使用
        screeningRepository.findAllByEndTimeBefore(now)
                .stream()
                .filter(s -> s.getStatus() != Screening.Status.COMPLETED)
                .forEach(screening -> {
                    screening.getTickets().stream().filter(
                            Ticket::isValid).forEach((ticket) ->
                            ticket.setStatus(Ticket.Status.USED));
                    screening.setStatus(Screening.Status.COMPLETED);
                    screeningRepository.save(screening);
                });
    }
}
